import os, sys
import random

sys.path.insert(0, os.path.expanduser('~') + '/lib/python')

from mylib.gl3d.vec3 import *
from OpenGL.GL import *
# Note Yet Supported
from OpenGL.GL.ARB.vertex_buffer_object import *
# http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_buffer_object.txt

import PIL.Image as Image  # PIL

try:
    import numpy as Numeric
except ImportError:
    try:
        import Numeric
    except ImportError:
        print("This demo requires the numpy or Numeric extension, sorry")
        exit(1)


class Face:
    Vertices = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    Normals = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    TexCoords = [[0, 0], [0, 0], [0, 0]]


class HeightMap:
    """HeightMap"""
    left = 0.0
    right = 0.0
    top = 0.0
    bottom = 0.0
    DT_VBO = 1
    DT_DL = 2
    DT_ARRAY = 3
    MT_MAZE = 'generate maze map'
    MT_IMAGE = 'read from image'
    MT_RANDOM = 'generate random map'

    def __init__(self, dt=DT_VBO):
        self.m_nVertexCount = 0  # // Vertex Count

        self.m_pVertices = None  # Numeric.array ( (), 'f') 		# // Vertex Data array
        self.m_pVertices_as_string = None  # raw memory string for VertexPointer ()

        self.m_pNormals = None  # Numeric.array ( (), 'f') 		# // Vertex Data array
        self.m_pNormals_as_string = None  # raw memory string for VertexPointer ()

        self.m_pTexCoords = None  # Numeric.array ( (), 'f') 	# // Texture Coordinates array
        self.m_pTexCoords_as_string = None  # raw memory string for TexPointer ()
        self.useVBO = False
        self.useDisplayList = False
        if dt == self.DT_VBO:
            self.useVBO = True
        elif dt == self.DT_DL:
            self.useDisplayList = True

        self.gl_list = None

        # // Vertex Buffer Object Names
        self.m_nVBOVertices = None  # // Vertex VBO Name
        self.m_nVBONormals = None  # // Normal VBO Name
        self.m_nVBOTexCoords = None  # // Texture Coordinate VBO Name

        # // Temporary Data
        self.m_nTextureId = None  # // Texture ID
        self.m_pTextureImage = None  # // Heightmap Data
        self.scale_xz = 1.0
        self.scale_y = 1.0
        self.size_x = 0
        self.size_y = 0
        self.half_x = 0.0
        self.half_y = 0.0
        self.ppb = 1
        self.pixels = []
        self.faces = []
        self.minimum = -9999

    def __del__(self):
        if self.gl_list is not None: glDeleteLists(self.gl_list, 1)

    def set_heightmap(self, width, height, data):
        self.size_x = width
        self.size_y = height
        self.half_x = width / 2.0
        self.half_y = height / 2.0

        self.pixels = list(data)

    def make_maze(self, m, width, height, sq=4):
        self.size_x = width * sq
        self.size_y = height * sq
        self.half_x = self.size_x / 2.0
        self.half_y = self.size_y / 2.0
        mlen = self.size_x * self.size_y
        for i in range(mlen):
            self.pixels.append(0.0)

        for yy in range(height):
            for xx in range(width):
                p = xx + yy * width
                for sqy in range(sq):
                    for sqx in range(sq):
                        sqp = (xx * sq + sqx) + (yy * sq + sqy) * width * sq
                        if sqp < mlen:
                            self.pixels[sqp] = m[p] * sq

    def make_random(self, width=128, height=128, repeat=16, radius=16):
        self.size_x = width
        self.size_y = height
        self.half_x = width / 2.0
        self.half_y = height / 2.0

        for i in range(width * height):
            self.pixels.append(0.0)

        # random.randrange(min(width, height) // 32, min(width, height) // 8)
        if radius is None: rad = min(width, height) // 10
        else: rad = radius
        for i in range(repeat):
            px = random.randrange(width)
            py = random.randrange(height)
            sx = px - rad
            sy = py - rad
            ex = px + rad
            ey = py + rad
            if sx < 0: sx = 0
            if sy < 0: sy = 0
            if ex > width: ex = width
            if ey > height: ey = height
            for y_ in range(sy, ey, 1):
                for x_ in range(sx, ex, 1):
                    dist = math.hypot(px - x_, py - y_)
                    if dist < rad:
                        p = x_ + y_ * width
                        self.pixels[p] += (10.0 * (rad - dist) / rad)
            '''
            while sy < ey:
                while sx < ex:
                    # if math.hypot(px - sx, py - sy) < radius:
                    p = sx + sy * width
                    self.pixels[p] += 5.0
                    if self.pixels[p] > 100.0: self.pixels[p] = 100.0
                    sx += 1
                sy += 1
            '''

    def load(self, filename):
        """ // Heightmap Loader """
        # // Error-Checking
        # // Load Texture Data
        try:
            self.m_pTextureImage = Image.open(filename)  # // Open The Image
            pixel = self.m_pTextureImage.getpixel((0, 0))
            self.ppb = len(pixel)
        except:
            print('Height Map open Fail!!! (%s)' % filename)
            return False

        # // Generate Vertex Field
        self.size_x = self.m_pTextureImage.size[0]
        self.size_y = self.m_pTextureImage.size[1]
        self.half_x = self.size_x / 2.0
        self.half_y = self.size_y / 2.0
        for _y in range(self.size_y - 1, -1, -1):
            for _x in range(self.size_x):
                _h = self._get_pixel(_x, _y)
                self.pixels.append(_h)

        print('Image Loaded - size(%dx%d) ppb(%d)' % (self.size_x, self.size_y, self.ppb))
        self.m_pTextureImage = None
        return True

    def _get_pixel(self, ix, iy):
        """ // Calculate The Position In The Texture, Careful Not To Overflow """
        if ix < 0: return 0
        if ix >= self.size_x: return 0
        if iy < 0: return 0
        if iy >= self.size_y: return 0

        # Get The Red, Green, and Blue Components
        # NOTE, Python Image library starts 0 at the top of the image - so to match the windows
        # code we reverse the Y order
        # pixel = self.m_pTextureImage.getpixel((nX, self.size_y - nY - 1))
        pixel = self.m_pTextureImage.getpixel((ix, iy))

        if self.ppb == 3:
            fr = float(pixel[0])
            fg = float(pixel[1])
            fb = float(pixel[2])
        else:
            fr = pixel
            fg = pixel
            fb = pixel
        # pixel = self.m_pTextureImage.getpixel((nY, nX))

        # // Calculate The Height Using The Luminance Algorithm
        return (0.299 * fr) + (0.587 * fg) + (0.114 * fb)

    def get_height(self, fx, fz):
        fy = 0.0
        if self.left > fx: return fy
        if self.right < fx: return fy
        if self.top > fz: return fy
        if self.bottom < fz: return fy
        fx = fx / self.scale_xz + self.half_x
        fz = fz / self.scale_xz + self.half_y
        p = math.floor(fx) + math.floor(fz) * self.size_x
        if p < len(self.pixels): fy = self.pixels[p] * self.scale_y
        return fy

    def generate(self, f_resolution=1.0, f_height_scale=1.0, f_size_scale=1.0):
        pixel_len = len(self.pixels)
        self.m_nVertexCount = int(self.size_x * self.size_y * 6 / (f_resolution * f_resolution))
        self.m_pVertices = Numeric.zeros((self.m_nVertexCount, 3), 'f')  # // Vertex Data
        self.m_pNormals = Numeric.zeros((self.m_nVertexCount, 3), 'f')  # // Normal Data
        self.m_pTexCoords = Numeric.zeros((self.m_nVertexCount, 2), 'f')  # // Texture Coordinates
        nIndex = 0
        i_resolution = int(f_resolution)
        self.left = -self.half_x * f_size_scale
        self.right = self.half_x * f_size_scale
        self.top = -self.half_y * f_size_scale
        self.bottom = self.half_y * f_size_scale
        self.scale_xz = f_size_scale
        self.scale_y = f_height_scale
        nZ = 0
        while nZ < self.size_y:
            nX = 0
            while nX < self.size_x:
                for nTri in range(6):
                    # // Using This Quick Hack, Figure The X,Z Position Of The Point
                    flX = float(nX)
                    flZ = float(nZ)
                    if nTri in (1, 2, 5): flX += f_resolution
                    if nTri in (2, 4, 5): flZ += f_resolution
                    x = (flX - self.half_x) * f_size_scale
                    z = (flZ - self.half_y) * f_size_scale
                    p = math.floor(flX) + math.floor(flZ) * self.size_x
                    if p < pixel_len: y = self.pixels[p] * f_height_scale
                    else: y = 0.0

                    self.m_pVertices[nIndex, 0] = x
                    self.m_pVertices[nIndex, 1] = y
                    self.m_pVertices[nIndex, 2] = z

                    # xyz = max(abs(x), abs(y), abs(z))
                    # self.m_pNormals[nIndex, 0] = x / xyz
                    # self.m_pNormals[nIndex, 1] = y / xyz
                    # self.m_pNormals[nIndex, 2] = z / xyz

                    self.m_pNormals[nIndex, 0] = 0.0
                    self.m_pNormals[nIndex, 1] = 1.0
                    self.m_pNormals[nIndex, 2] = 0.0

                    self.m_pTexCoords[nIndex, 0] = flX / self.size_x
                    self.m_pTexCoords[nIndex, 1] = flZ / self.size_y
                    nIndex += 1

                nX += i_resolution
            print(nZ)
            nZ += i_resolution

        print('Height Map Generated.')
        self._build_display_data()

        return True

    def generate2(self, f_resolution=1.0, f_height_scale=1.0, f_size_scale=1.0):
        pixel_len = len(self.pixels)
        self.m_nVertexCount = int(self.size_x * self.size_y * 24 / (f_resolution * f_resolution))
        # self.m_pVertices = Numeric.zeros ((self.m_nVertexCount * 3), 'f') 			# // Vertex Data
        # Non strings approach
        self.m_pVertices = Numeric.zeros((self.m_nVertexCount, 3), 'f')  # // Vertex Data
        self.m_pNormals = Numeric.zeros((self.m_nVertexCount, 3), 'f')  # // Vertex Data
        self.m_pTexCoords = Numeric.zeros((self.m_nVertexCount, 2), 'f')  # // Texture Coordinates
        nIndex = 0
        i_resolution = int(f_resolution)
        self.left = -self.half_x * f_size_scale
        self.right = self.half_x * f_size_scale
        self.top = -self.half_y * f_size_scale
        self.bottom = self.half_y * f_size_scale
        self.scale_xz = f_size_scale
        self.scale_y = f_height_scale
        nZ = 0
        while nZ < self.size_y:
            nX = 0
            while nX < self.size_x:
                for nTri in range(24):
                    # // Using This Quick Hack, Figure The X,Z Position Of The Point
                    flX = float(nX)
                    flZ = float(nZ)
                    if nTri in (2, 4, 5, 7, 8, 10): flX += (f_resolution / 2.0)
                    if nTri in (14, 16, 17, 19, 20, 22): flX -= (f_resolution / 2.0)
                    if nTri in (8, 10, 11, 13, 14, 16): flZ += (f_resolution / 2.0)
                    if nTri in (20, 22, 23, 1, 2, 4): flZ -= (f_resolution / 2.0)
                    x = (flX - self.half_x) * f_size_scale
                    z = (flZ - self.half_y) * f_size_scale
                    p = math.floor(flX) + math.floor(flZ) * self.size_x
                    if p < pixel_len: y = self.pixels[p] * f_height_scale
                    else: y = 0.0

                    self.m_pVertices[nIndex, 0] = x
                    self.m_pVertices[nIndex, 1] = y
                    self.m_pVertices[nIndex, 2] = z

                    # xyz = max(abs(x), abs(y), abs(z))
                    # self.m_pNormals[nIndex, 0] = x / xyz
                    # self.m_pNormals[nIndex, 1] = y / xyz
                    # self.m_pNormals[nIndex, 2] = z / xyz

                    self.m_pNormals[nIndex, 0] = 0.0
                    self.m_pNormals[nIndex, 1] = 1.0
                    self.m_pNormals[nIndex, 2] = 0.0

                    self.m_pTexCoords[nIndex, 0] = flX / self.size_x
                    self.m_pTexCoords[nIndex, 1] = flZ / self.size_y
                    nIndex += 1

                nX += i_resolution
            print(nZ)
            nZ += i_resolution

        print('Height Map Generated.')
        self._build_display_data()

        return True

    def generate3(self, f_resolution=1.0, f_height_scale=1.0, f_size_scale=1.0):
        pixel_len = len(self.pixels)
        self.m_nVertexCount = int(self.size_x * self.size_y * 24 / (f_resolution * f_resolution))
        # self.m_pVertices = Numeric.zeros ((self.m_nVertexCount * 3), 'f') 			# // Vertex Data
        # Non strings approach
        self.m_pVertices = Numeric.zeros((self.m_nVertexCount, 3), 'f')  # // Vertex Data
        self.m_pNormals = Numeric.zeros((self.m_nVertexCount, 3), 'f')  # // Vertex Data
        self.m_pTexCoords = Numeric.zeros((self.m_nVertexCount, 2), 'f')  # // Texture Coordinates
        nIndex = 0
        i_resolution = int(f_resolution)
        self.left = -self.half_x * f_size_scale
        self.right = self.half_x * f_size_scale
        self.top = -self.half_y * f_size_scale
        self.bottom = self.half_y * f_size_scale
        self.scale_xz = f_size_scale
        self.scale_y = f_height_scale
        nZ = 0
        while nZ < self.size_y:
            nX = 0
            while nX < self.size_x:
                pnt = self.get_triangle_point(nX, nZ, f_resolution)
                for i in range(len(pnt)):
                    p = math.floor(pnt[i].x) + math.floor(pnt[i].z) * self.size_x
                    if p < pixel_len: pnt[i].y = self.pixels[p] * f_height_scale
                    else: pnt[i].y = 0.0

                for idx in range(8):
                    nml = get_face_normal(pnt[9], pnt[idx], pnt[idx + 1], f_size_scale)
                    nml = -nml
                    for i in (9, idx, idx + 1):
                        self.m_pVertices[nIndex, 0] = (pnt[i].x - self.half_x) * f_size_scale
                        self.m_pVertices[nIndex, 1] = pnt[i].y
                        self.m_pVertices[nIndex, 2] = (pnt[i].z - self.half_y) * f_size_scale
                        self.m_pNormals[nIndex, 0] = nml.x
                        self.m_pNormals[nIndex, 1] = nml.y
                        self.m_pNormals[nIndex, 2] = nml.z
                        self.m_pTexCoords[nIndex, 0] = pnt[i].x / self.size_x
                        self.m_pTexCoords[nIndex, 1] = pnt[i].z / self.size_y
                        nIndex += 1

                nX += i_resolution
            print(nZ)
            nZ += i_resolution

        print('Height Map Generated. %d vertices' % nIndex)
        self._build_display_data()

        return True

    def generate_faces(self, f_resolution=1.0, f_height_scale=1.0, f_size_scale=1.0):
        self.useDisplayList = True
        pixel_len = len(self.pixels)
        i_resolution = int(f_resolution)
        self.left = -self.half_x * f_size_scale
        self.right = self.half_x * f_size_scale
        self.top = -self.half_y * f_size_scale
        self.bottom = self.half_y * f_size_scale
        self.scale_xz = f_size_scale
        self.scale_y = f_height_scale
        nZ = 0
        while nZ < self.size_y:
            nX = 0
            while nX < self.size_x:
                pnt = self.get_triangle_point(nX, nZ, f_resolution)
                for i in range(len(pnt)):
                    p = math.floor(pnt[i].x) + math.floor(pnt[i].z) * self.size_x
                    if p < pixel_len: pnt[i].y = self.pixels[p] * f_height_scale
                    else: pnt[i].y = 0.0

                for idx in range(8):
                    nml = get_face_normal(pnt[9], pnt[idx], pnt[idx + 1], f_size_scale)
                    nml = -nml
                    face = Face()
                    fi = 0
                    for i in (9, idx, idx + 1):
                        face.Vertices[fi][0] = (pnt[i].x - self.half_x) * f_size_scale
                        face.Vertices[fi][1] = pnt[i].y
                        face.Vertices[fi][2] = (pnt[i].z - self.half_y) * f_size_scale
                        face.Normals[fi][0] = nml.x
                        face.Normals[fi][1] = nml.y
                        face.Normals[fi][2] = nml.z
                        face.TexCoords[fi][0] = pnt[i].x / self.size_x
                        face.TexCoords[fi][1] = pnt[i].z / self.size_y
                        fi += 1
                    self.faces.append(face)
                nX += i_resolution
            print(nZ)
            nZ += i_resolution

        print('Height Map Generated. %d face' % len(self.faces))
        self._build_display_data()

        return True

    @staticmethod
    def get_triangle_point(x_, z_, r_):
        r_ /= 2.0
        return [Vec3d(x_ - r_, 0, z_ - r_), Vec3d(x_, 0, z_ - r_), Vec3d(x_ + r_, 0, z_ - r_),
                Vec3d(x_ + r_, 0, z_), Vec3d(x_ + r_, 0, z_ + r_), Vec3d(x_, 0, z_ + r_),
                Vec3d(x_ - r_, 0, z_ + r_), Vec3d(x_ - r_, 0, z_), Vec3d(x_ - r_, 0, z_ - r_),
                Vec3d(x_, 0, z_)]

    def _build_display_data(self):
        """ // Generate And Bind The Vertex Buffer """
        if self.useVBO:
            print('Build VBO Buffer')
            self.m_nVBOVertices = int(glGenBuffersARB(1))  # // Get A Valid Name
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, self.m_nVBOVertices)  # // Bind The Buffer
            # // Load The Data
            glBufferDataARB(GL_ARRAY_BUFFER_ARB, self.m_pVertices, GL_STATIC_DRAW_ARB)

            self.m_nVBONormals = int(glGenBuffersARB(1))  # // Get A Valid Name
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, self.m_nVBONormals)  # // Bind The Buffer
            # // Load The Data
            glBufferDataARB(GL_ARRAY_BUFFER_ARB, self.m_pNormals, GL_STATIC_DRAW_ARB)

            # // Generate And Bind The Texture Coordinate Buffer
            self.m_nVBOTexCoords = int(glGenBuffersARB(1))  # // Get A Valid Name
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, self.m_nVBOTexCoords)  # // Bind The Buffer
            # // Load The Data
            glBufferDataARB(GL_ARRAY_BUFFER_ARB, self.m_pTexCoords, GL_STATIC_DRAW_ARB)

        elif self.useDisplayList:
            self.gl_list = glGenLists(1)
            glNewList(self.gl_list, GL_COMPILE)
            glBegin(GL_TRIANGLES)
            '''
            for f in self.faces:
                glNormal3fv(f.Normals[0])
                glTexCoord2fv(f.TexCoords[0])
                glVertex3fv(f.Vertices[0])
                glNormal3fv(f.Normals[1])
                glTexCoord2fv(f.TexCoords[1])
                glVertex3fv(f.Vertices[1])
                glNormal3fv(f.Normals[2])
                glTexCoord2fv(f.TexCoords[2])
                glVertex3fv(f.Vertices[2])
            '''
            for idx in range(self.m_nVertexCount):
                glTexCoord2f(self.m_pTexCoords[idx, 0], self.m_pTexCoords[idx, 1])
                glNormal3f(self.m_pNormals[idx, 0], self.m_pNormals[idx, 1], self.m_pNormals[idx, 2])
                glVertex3f(self.m_pVertices[idx, 0], self.m_pVertices[idx, 1], self.m_pVertices[idx, 2])

            glEnd()
            glEndList()
            print('Display List %d Created.' % self.gl_list)
        else:
            self.m_pVertices_as_string = self.m_pVertices.tostring()
            self.m_pNormals_as_string = self.m_pNormals.tostring()
            self.m_pTexCoords_as_string = self.m_pTexCoords.tostring()

        # // Our Copy Of The Data Is No Longer Necessary, It Is Safe In The Graphics Card
        self.m_pVertices = None
        self.m_pNormals = None
        self.m_pTexCoords = None

        return

    def render(self):

        if self.useVBO:
            # // Enable Pointers
            glEnableClientState(GL_VERTEX_ARRAY)  # // Enable Vertex Arrays
            glEnableClientState(GL_NORMAL_ARRAY)  # // Enable Vertex Arrays
            glEnableClientState(GL_TEXTURE_COORD_ARRAY)  # // Enable Texture Coord Arrays
            glFrontFace(GL_CCW)

            glBindBufferARB(GL_ARRAY_BUFFER_ARB, self.m_nVBOVertices)
            glVertexPointer(3, GL_FLOAT, 0, None)  # // Set The Vertex Pointer To The Vertex Buffer
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, self.m_nVBONormals)
            glNormalPointer(GL_FLOAT, 0, None)  # // Set The Vertex Pointer To The Vertex Buffer
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, self.m_nVBOTexCoords)
            glTexCoordPointer(2, GL_FLOAT, 0, None)  # // Set The TexCoord Pointer To The TexCoord Buffer

            glDrawArrays(GL_TRIANGLES, 0, self.m_nVertexCount)  # // Draw All Of The Triangles At Once

            # // Disable Pointers
            glDisableClientState(GL_VERTEX_ARRAY)  # // Disable Vertex Arrays
            glDisableClientState(GL_NORMAL_ARRAY)  # // Disable Vertex Arrays
            glDisableClientState(GL_TEXTURE_COORD_ARRAY)  # // Disable Texture Coord Arrays
        elif self.useDisplayList:
            glCallList(self.gl_list)
        else:
            # You can use the pythonism glVertexPointerf (), which will convert the numarray into
            # the needed memory for VertexPointer. This has two drawbacks however:
            #  1) This does not work in Python 2.2 with PyOpenGL 2.0.0.44
            #  2) In Python 2.3 with PyOpenGL 2.0.1.07 this is very slow.
            # See the PyOpenGL documentation. Section "PyOpenGL for OpenGL Programmers" for details
            # regarding glXPointer API.
            # Also see OpenGLContext Working with Numeric Python
            # glVertexPointerf ( g_pMesh.m_pVertices ); 	# // Set The Vertex Pointer To Our Vertex Data
            # glTexCoordPointerf ( g_pMesh.m_pTexCoords ); 	# // Set The Vertex Pointer To Our TexCoord Data
            #
            #
            # The faster approach is to make use of an opaque "string" that represents the
            # the data (vertex array and tex coordinates in this case).
            # // Enable Pointers
            glEnableClientState(GL_VERTEX_ARRAY)  # // Enable Vertex Arrays
            glEnableClientState(GL_NORMAL_ARRAY)  # // Enable Normal Arrays
            glEnableClientState(GL_TEXTURE_COORD_ARRAY)  # // Enable Texture Coord Arrays
            glFrontFace(GL_CCW)

            # // Set The Vertex Pointer To Our Vertex Data
            glVertexPointer(3, GL_FLOAT, 0, self.m_pVertices_as_string)
            # // Set The Vertex Pointer To Our Normal Data
            glNormalPointer(GL_FLOAT, 0, self.m_pNormals_as_string)
            # // Set The Vertex Pointer To Our TexCoord Data
            glTexCoordPointer(2, GL_FLOAT, 0, self.m_pTexCoords_as_string)

            glDrawArrays(GL_TRIANGLES, 0, self.m_nVertexCount)  # // Draw All Of The Triangles At Once

            # // Disable Pointers
            glDisableClientState(GL_VERTEX_ARRAY)  # // Disable Vertex Arrays
            glDisableClientState(GL_NORMAL_ARRAY)  # // Disable Normal Arrays
            glDisableClientState(GL_TEXTURE_COORD_ARRAY)  # // Disable Texture Coord Arrays
